// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl;

import com.intellij.codeInsight.daemon.LineMarkerInfo;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.impl.RangeMarkerImpl;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.MarkupEditorFilter;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.TextRangeScalarUtil;
import com.intellij.psi.PsiElement;
import com.intellij.util.Consumer;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

final class LineMarkersUtil {
  private static final Logger LOG = Logger.getInstance(LineMarkersUtil.class);
  private static final Object LOCK = ObjectUtils.sentinel(LineMarkersUtil.class.getName());

  static void setLineMarkersToEditor(@NotNull Project project,
                                     @NotNull Document document,
                                     @NotNull TextRange bounds,
                                     @NotNull Collection<? extends LineMarkerInfo<?>> newMarkers,
                                     int group,
                                     @NotNull HighlightingSession highlightingSession) {
    ApplicationManager.getApplication().assertIsNonDispatchThread();
    ApplicationManager.getApplication().assertReadAccessAllowed();

    MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true);
    Long2ObjectMap<List<RangeHighlighterEx>> recycler = new Long2ObjectOpenHashMap<>();  // range -> list of highlighters in this range; these are loose highlighters (ones which are generated by some non-managed pass, meaning other than GHP/LIP)
    markupModel.processRangeHighlightersOverlappingWith(bounds.getStartOffset(), bounds.getEndOffset(),
      highlighter -> {
        LineMarkerInfo<?> info = getLineMarkerInfo(highlighter);

        if (
          // (recycle) zombie line marker immediately because similar-looking line markers don't merge, unlike regular HighlightInfos
          HighlightingNecromancer.isZombieMarkup(highlighter) && highlighter.getGutterIconRenderer() != null
          || group == -1 || info != null && info.updatePass == group) {
          recycler.computeIfAbsent(((RangeMarkerImpl)highlighter).getScalarRange(), __ -> new ArrayList<>()).add(highlighter);
        }
        return true;
      }
    );

    for (LineMarkerInfo<?> info : newMarkers) {
      PsiElement element = info.getElement();
      if (element == null) {
        continue;
      }

      TextRange textRange = element.getTextRange();
      if (textRange == null) continue;
      TextRange elementRange = InjectedLanguageManager.getInstance(project).injectedToHost(element, textRange);
      if (bounds.intersects(elementRange)) {
        createOrReuseLineMarker(info, markupModel, recycler);
      }
    }
    incinerate(recycler);
    if (LOG.isDebugEnabled()) {
      LOG.debug("LineMarkersUtil.setLineMarkersToEditor(" +bounds+
                "; newMarkers: " + newMarkers + ", group: " + group + "); reused: " + recycler.size());
    }
  }

  private static void incinerate(@NotNull Long2ObjectMap<List<RangeHighlighterEx>> recycler) {
    for (List<RangeHighlighterEx> value : recycler.values()) {
      for (RangeHighlighterEx highlighter : value) {
        highlighter.dispose();
      }
    }
    recycler.clear();
  }

  private static void createOrReuseLineMarker(@NotNull LineMarkerInfo<?> info,
                                              @NotNull MarkupModelEx markupModel,
                                              @NotNull Long2ObjectMap<List<RangeHighlighterEx>> toReuse) {
    LineMarkerInfo.LineMarkerGutterIconRenderer<?> newRenderer = (LineMarkerInfo.LineMarkerGutterIconRenderer<?>)info.createGutterRenderer();
    List<RangeHighlighterEx> list = toReuse.get(TextRangeScalarUtil.toScalarRange(info.startOffset, info.endOffset));
    RangeHighlighterEx highlighter = list == null ? null : ContainerUtil.find(list, r -> r.getLayer() == HighlighterLayer.ADDITIONAL_SYNTAX);
    if (highlighter == null) {
      highlighter = markupModel.addRangeHighlighterAndChangeAttributes(
        null, info.startOffset, info.endOffset,
        HighlighterLayer.ADDITIONAL_SYNTAX, HighlighterTargetArea.LINES_IN_RANGE, false,
        changeAttributes(info, true, newRenderer, true, true));

      MarkupEditorFilter editorFilter = info.getEditorFilter();
      if (editorFilter != MarkupEditorFilter.EMPTY) {
        highlighter.setEditorFilter(editorFilter);
      }
    }
    else {
      list.remove(highlighter);
      LineMarkerInfo.LineMarkerGutterIconRenderer<?> oldRenderer = highlighter.getGutterIconRenderer() instanceof LineMarkerInfo.LineMarkerGutterIconRenderer<?> line ? line : null;
      boolean rendererChanged = newRenderer == null || !newRenderer.equals(oldRenderer);
      boolean lineSeparatorColorChanged = !Comparing.equal(highlighter.getLineSeparatorColor(), info.separatorColor);
      boolean lineSeparatorPlacementChanged = !Comparing.equal(highlighter.getLineSeparatorPlacement(), info.separatorPlacement);

      if (rendererChanged || lineSeparatorColorChanged || lineSeparatorPlacementChanged) {
        markupModel.changeAttributesInBatch(highlighter, changeAttributes(info, rendererChanged, newRenderer, lineSeparatorColorChanged, lineSeparatorPlacementChanged));
      }
      HighlightingNecromancer.unmarkZombieMarkup(highlighter);
    }
    highlighter.putUserData(LINE_MARKER_INFO, info);
    info.highlighter = highlighter;
  }

  private static @NotNull Consumer<RangeHighlighterEx> changeAttributes(@NotNull LineMarkerInfo<?> info,
                                                                        boolean rendererChanged,
                                                                        LineMarkerInfo.LineMarkerGutterIconRenderer<?> newRenderer,
                                                                        boolean lineSeparatorColorChanged,
                                                                        boolean lineSeparatorPlacementChanged) {
    return markerEx -> {
      if (rendererChanged) {
        markerEx.setGutterIconRenderer(newRenderer);
      }
      if (lineSeparatorColorChanged) {
        markerEx.setLineSeparatorColor(info.separatorColor);
      }
      if (lineSeparatorPlacementChanged) {
        markerEx.setLineSeparatorPlacement(info.separatorPlacement);
      }
    };
  }

  static void addLineMarkerToEditorIncrementally(@NotNull Project project, @NotNull Document document, @NotNull LineMarkerInfo<?> markerInfo) {
    ApplicationManager.getApplication().assertIsNonDispatchThread();
    ApplicationManager.getApplication().assertReadAccessAllowed();

    MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true);
    LineMarkerInfo<?>[] markerInTheWay = {null};
    Long2ObjectMap<List<RangeHighlighterEx>> recycler = new Long2ObjectOpenHashMap<>();  // range -> list of highlighters in this range; these are loose highlighters (ones which are generated by some non-managed pass, meaning other than GHP/LIP)

    boolean allIsClear;
    synchronized (LOCK) {
      allIsClear = markupModel.processRangeHighlightersOverlappingWith(markerInfo.startOffset, markerInfo.endOffset,
        highlighter -> {
          if (HighlightingNecromancer.isZombieMarkup(highlighter)) {
            recycler.computeIfAbsent(((RangeMarkerImpl)highlighter).getScalarRange(), __ -> new ArrayList<>()).add(highlighter);
            return true;
          }
          LineMarkerInfo<?> info = getLineMarkerInfo(highlighter);
          if (info != null) {
            markerInTheWay[0] = info;
            return false;
          }
          return true;
        });
      if (allIsClear) {
        createOrReuseLineMarker(markerInfo, markupModel, recycler);
      }
    }
    incinerate(recycler);

    if (LOG.isDebugEnabled()) {
      LOG.debug("addLineMarkerToEditorIncrementally: "+markerInfo+" "+(allIsClear ? "created" : " (was not added because "+markerInTheWay[0] +" was in the way)"));
    }
  }

  static @Nullable LineMarkerInfo<?> getLineMarkerInfo(@NotNull RangeHighlighter highlighter) {
    return highlighter.getUserData(LINE_MARKER_INFO);
  }

  private static final Key<LineMarkerInfo<?>> LINE_MARKER_INFO = Key.create("LINE_MARKER_INFO");
}
